流程图简介
这个流程图可以被原样输出到PDF中,也可以被原样渲染成html页面
Typora之所以可以画流程图是因为支持mermaid语言,所以使用只需要插入代码块,把语言调成mermaid即可
官方的介绍网址:https://mermaid-js.github.io/mermaid/#/flowchart?id=a-hexagon-node
Mermaid是一个用于画流程图、状态图、时序图、甘特图的库,使用 JS 进行本地渲染,广泛集成于许多Markdown编辑器中
流程图代码示例
网页嵌入示例
Emoji作为文档标记使用,Emoji是一种文本类型的象形符号,Emoji中文网可以检索所有的Emoji图标Emoji中文网
这个是MarkDown官方文档推荐的Emoji检索工具📙 Emojipedia — 😃 Home of Emoji Meanings 💁👌🎍😍
文档通用型标记
📻:Emoji使用格式
Emoji | 作用说明 | Emoji | 作用说明 |
---|---|---|---|
📻 | 可扩展内容 | 🔎 | 备注说明 |
📜 | 需求 | 💡 | 需求实现方案 |
📌 | 异常/反常现象 | 🔑 | 解释异常/反常/存疑 |
🚁 | 记忆点标题 | 📓 | 记忆点内容 |
👅 | 未确认猜想 | ❓ | 存疑 |
流程步骤标注
Emoji | 作用说明 | Emoji | 作用说明 |
---|---|---|---|
1️⃣ | 步骤1 | 2️⃣ | 步骤2 |
3️⃣ | 步骤3 | 4️⃣ | 步骤4 |
5️⃣ | 步骤5 | 6️⃣ | 步骤6 |
7️⃣ | 步骤7 | 8️⃣ | 步骤8 |
9️⃣ | 步骤9 | 🔟 | 步骤10 |
0️⃣ | 步骤0 | 🔁[1️⃣3️⃣] | 循环步骤1和步骤3 |
Emoji使用格式
📻:这里写对应的内容
🔎:支持缩进和组合使用
HTML实体非常多,以下只列举常用的实体,完整内容参考:HTML 4 实体名称 | 菜鸟教程 (runoob.com)
实体 | 对应字符 | 说明 |
---|---|---|
© | © | 版权 |
® | ® | 注册商标 |
™ | ™ | 商标 |
& | & | 与符号 |
| 空格 | |
Á | Á |
参数格式
log_format
参数值: log_format name [escape=default|json|none] string ...;
默认值: log_format combined "...";
作用域: http
参数解析:配置日志的格式
🔎:可以配置要记录的变量,如下实例所示$remote_addr
是远程IP地址、$time_local
是本地时间、$status
是请求的状态,还有以前使用过的各种变量,貌似官网列举的变量不是全部,老师说正常的Nginx变量都可以用
🔎:日志的格式除了支持文本的方式还支持json的格式的字符串,但是nginx并不知道变量的层级关系,只会去做特殊字符的转义
配置实例:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
指令中使用如[-s]
表示-s
参数是可选配置,且是一种不需要指定参数值的配置,[-t ttl]
也是可选配置,配置时需要指明-t
参数且需要指定参数值ttl
<path>
表示参数path
的参数值是必填项
指令的使用要点在示例中命令上方的注释中使用有序列表标识,没有进行标识的命令是辅助命令,指令的注释在命令上方
create [-s] [-e] [-c] [-t ttl] <path> [data] [acl]
功能解析:在指定路径位置创建一个节点,[data]
是指定节点的数据内容
使用示例:
xxxxxxxxxx
#1. 在根节点下创建一个名为aa的节点
[zk: localhost:2181(CONNECTED) 4] create /aa
Created /aa
#2. 节点已经存在的情况下再创建节点会直接报错节点已经存在,创建动作会失败,想要在创建的时候指定数据内容必须保证对应位置节点不存在
[zk: localhost:2181(CONNECTED) 6] create /aa test
Node already exists: /aa
[zk: localhost:2181(CONNECTED) 8] delete /aa
#3. 创建节点`/aa`并指定节点的数据为test
[zk: localhost:2181(CONNECTED) 9] create /aa test
Created /aa
[zk: localhost:2181(CONNECTED) 10] get /aa
test
#4. 创建节点时指定节点数据内容,如果数据中含有空格必须用双引号将数据整体括起来,否则空格后面的内容会被作为`acl`参数并且可能提示该参数非法,节点数据没有空格可以不加双引号
[zk: localhost:2181(CONNECTED) 11] create /aa/cc hello world
world does not have the form scheme:id:perm
KeeperErrorCode = InvalidACL
Acl is not valid : null
[zk: localhost:2181(CONNECTED) 12] create /aa/cc "hello world"
Created /aa/cc
[zk: localhost:2181(CONNECTED) 13] get /aa/cc
hello world
补充说明:
指定位置节点已经存在的前提下再创建该节点会报错
创建节点同时指定节点数据,如果数据中有空格需要使用双引号将数据完整括起来,否则空格后面的内容会被识别为acl参数
QueryWrapper<BrandEntity>--->queryWrapper.eq(String column,Object val)
功能解析:查询指定字段等于指定值的记录
使用示例:queryWrapper.eq("age",30);
示例含义:查询age字段等于30的记录
补充说明:
箭头前为返回值类型,方法调用者首字母小写为实例方法,方法调用者首字母大写为静态方法,补充说明没有可以不添加
【POST】http://192.168.56.10:9200/customer/external/1/_update
请求体
xxxxxxxxxx
{
"doc":{
"name": "John"
}
}
功能解析:根据文档数据的索引、类型、文档id和文档内容对文档数据进行更新,如果文档数据和ES服务器中的文档数据内容相同,多次操作ES服务器中的数据不会发生任何变化,连数据的版本号都不会发生变化,在响应内容的result为noop,表示什么都不做,然而不带_update
的更新操作不会检查原文档数据是否和需要更新后的文档数据是否一致;同时注意使用_update
进行更新,更新内容要放在请求体的doc属性中
响应内容:
xxxxxxxxxx
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 6,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 10,
"_primary_term": 1
}
【多次操作单文档数据不变的响应】
xxxxxxxxxx
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 6,
"result": "noop",
"_shards": {
"total": 0,
"successful": 0,
"failed": 0
},
"_seq_no": 10,
"_primary_term": 1
}
补充说明:
URI带_update
的更新请求只能是POST请求方式,不能使用PUT请求方式,且带_update
的POST请求是局部更新,即文档数据不会直接全部覆盖,有对应属性的数据相应修改,没有对应的属性就保留原文档数据,新增没有的属性和相应的数据;但是注意不带_update
的上述两种PUT和POST方式的更新都是全量更新,即直接用请求体的数据直接将原文档数据全部直接覆盖
对于内容确认,我们有如下规定样式原本不确定内容或者猜想内容【后期经确认的内容】
实现步骤和工作流程
1️⃣:先这样
2️⃣:然后这样
🔁[1️⃣2️⃣]:重复第一步和第二步直到满足条件
3️⃣:最后这样
JMeter报错Address Already in use
问题描述:Jmeter访问测试本机127.0.0.1的端口服务,在无限请求的情况下请求产生大量异常,异常率迅速飙升超过50%,响应体报错,提示信息Address already in use
原因分析:该问题实际是windows的问题,windows本身提供给TCP/IP的端口是1024-5000,且需要四分钟才会循环回收这些端口,短时间内跑大量的请求会将端口占满
解决方法1:修改windows的注册文件,windows官方文档中指出当尝试大于5000的TCP端口连接时会收到大量错误,可以通过以下方案来解决
1️⃣:在win+r打开窗口中使用命令regedit
打开注册表
2️⃣:选择计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
3️⃣:右击parameters
,新建--DWORD32位,修改对应名字为MaxUserPort,点击该MaxUserPort,在弹出窗口将数值改为65534,基数为十进制
🔎:这是设置最大可用端口数量
4️⃣:右击parameters
,新建--DWORD32位,修改对应名字为TCPTimedWaitDelay,点击该TCPTimedWaitDelay,在弹出窗口将数值改为30,基数为十进制
🔎:这是设置windows回收关闭端口的等待时间为30s
5️⃣:退出注册表编辑器,重启计算机配置才能生效
补充说明:对解决方法1进行补充说明,不需要补充说明该项可以不写
XXX测试
测试环境:
1️⃣:100个用户线程,1s内每个用户线程发起50次扣减库存请求,总共发起5000次,场景为请求连接nginx负载均衡两个运行实例操作虚拟机上的同一个数据库的同一个共享数据,查询数据库库存和更新数据库库存两条SQL简化为一条SQLupdate db_stock set count=count-#{count} where product_code=#{product_no} and count >= #{count}
2️⃣:在1️⃣的环境基础上再开启服务组件的多例模式
3️⃣:在1️⃣的环境基础上在服务中使用@Transactional
注解手动开启事务
测试结果:
1️⃣:吞吐量为1953,所有库存扣减请求全部成功,数据库库存被正确减为0
2️⃣:吞吐量为2224,所有库存扣减请求全部成功,数据库库存被正确减为0
3️⃣:吞吐量为1850,所有库存扣减请求全部成功,数据库库存被正确减为0
结果分析:
一条SQL因为mysql内部加了悲观锁,因此服务中不需要加锁,不需要加事务,集群下也能保证数据库共享数据的操作原子性,性能提升非常可观;不仅能满足集群部署无锁并发线程安全性,还不需要加事务[1],而且在服务组件单例或者多例模式下都能保证共享数据的并发线程安全,不管是锁、事务还是多例都能可观影响系统的并发性能
补充说明以🔎打头,后跟一个中文冒号,补充说明的内容与IDE的注释颜色保持一致,均为绿色,用来对上一级内容要点进行补充说明
🔎:这是对上述补充说明格式说明的补充说明
要点1
🔎:对要点1的补充说明
渲染代码::mag_right:<font color=green>:
...</font>
对一个词或者局部的简单说明使用[这种方式]进行简单说明
增加特定的样式表示该注释[以下代码是测试用例,不要在生产中使用]
这个部分需要在C:\Users\Earl\AppData\Roaming\Typora\themes\github.css
中添加动画的如下css
样式,Typora
官网说用户自定义的css
样式应该放在相同目录下的github.user.css
中避免Typora
更新将主题css
文件覆盖掉,但是我试了不能实现对应效果,最后还是在C:\Users\Earl\AppData\Roaming\Typora\themes\github.css
中进行的修改,而且想把整个样式抽取出来放在github.css
中并没有达到对应效果,这一块深入学了前端再来解决
xxxxxxxxxx
/* 用户自定义背景移动 */
@keyframes gradient{
0%{background-position:0 0;}
100%{background-position:-100% 0;}
}
补充说明使用示例
OpenResty中Lua脚本获取请求的通用信息
🔎:以下代码全部在hello.lua
脚本中
获取http协议版本ngx.req.http_version()
xxxxxxxxxx
ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "<br/>")
获取请求的方法ngx.req.get_method()
xxxxxxxxxx
ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "<br/>")
获取原始的请求头内容ngx.req.raw_header()
🔎:该方式获取的是最原始的整个请求头文本
xxxxxxxxxx
ngx.say("ngx.req.raw_header : ", ngx.req.raw_header(), "<br/>")
获取请求的请求体内容ngx.req.get_body_data()
xxxxxxxxxx
ngx.say("ngx.req.get_body_data() : ", ngx.req.get_body_data(), "<br/>")
对中间内容使用注的方式来进行旁注[1]或者说明[2]
一旦Mysql的一张表中在业务中有一个地方使用了select...for update
,其他业务逻辑不能使用普通的select来做业务避免出现并发性的问题
❓:上述观点存疑,意思是记录被锁住了,其他线程还能操作吗?还是说使用了select...for update
如果其他不使用for update
上锁的操作不会等待锁会直接失败?
🔑:明白了,是因为虽然线程1使用select...for update上
锁了,但是其他业务线程2如果使用普通的select
,select
操作不会被阻塞,只有更新操作会被阻塞【类似CopyOnWriteArrayList的读写并发】,但是如果其他业务将还没提交的数据读取到,在处理期间上锁的原业务已经提交了,此时做出的更改就是基于线程1上锁前的数据,而非线程1提交后的数据,此时多业务对共享数据的更新操作就会出现并发线程安全问题,因此使用select...for update
所有表下的业务都得使用select...for update
加锁方法
核心:这里面写的很绕但是核心逻辑还是和我们自己的实现差不多,使用ConcurrentHashMap
来存储锁重入计数信息,加锁先从ConcurrentHashMap
即threadData
通过当前线程获取锁重入计数对象LockData
,如果不为null
说明本次上锁是锁重入,直接原子整数累加1;如果LockData为null
就进入获取锁的阶段,会拼接出完整的节点对象路径并创建临时序列化节点,并获取子节点列表,判断创建的临时序列化节点的序列号在子节点列表中是否小于最大租约数,如果小于就获取锁成功,这里最大租约数默认写死就是1,所以只有最小的节点才能获取锁,返回获取锁的标志作为返回当前节点完整路径的标志并将路径了当前线程以及锁重入计数为1封装到LockData
中传递给threadData
并结束上锁方法;如果大于最大租约数就去监听当前节点下标-最大租约数的节点的删除事件,里面的事件监听并判断前驱节点是否仍然存在使用的getData
方法,比我们使用的exists
方法的优势在于exists
方法不论节点是否存在都会进行监听,但是getData
方法如果节点不存在就不会进行监听,这样可以避免客户端资源的泄露和浪费;面试按前面手动实现的过程结合这个流程来说即可
xprivate final ConcurrentMap<Thread, LockData> InterProcessMutex.threadData = Maps.newConcurrentMap();
private static class InterProcessMutex.LockData
{
final Thread owningThread;
final String lockPath;
final AtomicInteger lockCount = new AtomicInteger(1);
}
static final String CreateBuilderImpl.PROTECTED_PREFIX = "_c_";
-----------------------------------------------------------------------------------------------------
interProcessMutex.acquire()
/**
* Acquire the mutex - blocking until it's available. Note: the same thread
* can call acquire re-entrantly. Each call to acquire must be balanced by a call
* to {@link #release()}
*
* @throws Exception ZK errors, connection interruptions
*/
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )1️⃣ //加锁方法实际上调用的是internalLock(-1, null)方法
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
1️⃣ interProcessMutex.internalLock(-1, null)
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
Thread currentThread = Thread.currentThread();//获取当前线程
LockData lockData = threadData.get(currentThread);//通过属性threadData获取锁的重入信息LockData,threadData是InterProcessMutex类中的ConcurrentHashMap类型的属性,该Map保存了锁的重入信息,以当前线程作为Map的key,以LockData作为Map的value,LockData是InterProcessMutex中定义的一个静态内部类,其中有三个属性,Thread类型的属性owningThread表示当前LockData对象属于哪一个线程,String类型的属性lockPath记录当前线程创建的对应节点的路径,原子整数AtomicInteger类型的lockCount属性记录了当前线程对应的锁的重入次数,如果当前线程创建了多个不同路径的分布式锁,由于只有一个键值对,此前的LockData就会直接被新的锁对象的LockData覆盖掉导致先加的锁无法解锁,程序卡死
if ( lockData != null )//如果当前线程的LockData不为null,说明当前线程此前已经获取过锁了,直接对LockData的lockCount属性进行cas操作递增1然后上锁方法直接返回
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());1️⃣-2️⃣ //如果当前线程的LockData不为null,调用internals.attemptLock()方法尝试获取锁,获取到锁返回lockPath,如果成功获取到锁,使用当前线程和lockPath来构造当前线程的LockData对象,LockData的构造方法中默认就使用1来赋值重入次数计数;并将该对象存入threadData,这个threadData就像老杜讲的那个ThreadLocal,老杜瞎几把讲,真实的ThreadLocal根本不是他说的那个模型,然后结束方法执行返回true表示获取锁成功
if ( lockPath != null )
{
LockData newLockData = new LockData(currentThread, lockPath);//这里面第一次获取锁是直接将锁重入计数设置为1,并向LockData中放入当前线程和锁路径[锁路径是完整的节点路径]
threadData.put(currentThread, newLockData);//将LockData对象以当前线程作为key存入保存锁重入信息的threadData即ConcurrentHashMap中,至此获取锁执行结束返回true给acquire方法结束Acquire方法的执行,后续锁重入就从该Map中获取锁重入信息直接计数加1,上面讲过了
return true;
}
//尝试获取锁失败,lockPath为null获取锁的acquire方法就直接返回false了
return false;
}
1️⃣-2️⃣ lockInternals.attemptLock(time, unit, getLockNodeBytes())
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
final long startMillis = System.currentTimeMillis();//获取系统当前时间
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;//获取锁的等待时间
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0;//重试次数
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;//该变量设置为false,在下面的while循环中死循环来尝试获取锁
while ( !isDone )
{
isDone = true;//首先将isDone设置为true,如果获取锁成功就直接退出循环,获取锁失败了就将其设置为false继续死循环
try
{
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);1️⃣-2️⃣-1️⃣ //通过该方法为当前要获取的锁创建一个节点并返回节点的全路径,这里面只是拼接出完成的节点路径[returnPath: "/curator/locks/_c_09c8be8f-a044-4e1d-a1c7-f50bd4571b8b-lock-"],第一次进入循环执行完下面一行代码会直接抛异常进入catch语句块设置isDone为false再次进入循环,此时Zookeeper中还没有创建对应路径的节点,在第二次进入循环执行完该代码时Zookeeper中的对应路径节点就创建出来了,所以获取锁的方法还是在该方法中,具体在哪儿老师没有追进去,可能在forPath方法前面,第一次循环发现路径格式不对所以没创建出来节点,在下面方法检查锁是否存在时发现不存在因而抛出异常再次进入循环发现路径对了
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);1️⃣-2️⃣-2️⃣ //第一次循环debug在执行这一步的过程中直接抛出异常进入catch语句块中了,这个情况比较复杂,有第一次直接抛异常进入后续catch块的,也有第一次执行异常被自身捕获处理后返回false的,老师已经混乱开始瞎讲了,
}
catch ( KeeperException.NoNodeException e )//如果上面的代码执行出现异常就会进入catch逻辑
{
// gets thrown by StandardLockInternalsDriver when it can't find the lock node
// this can happen when the session expires, etc. So, if the retry allows, just try it all again
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;//进入catch语句块将isDone设置为false,再次进入循环,成功获取锁不会抛异常,所以成功获取锁会直接结束循环返回lockPath并封装到threadData中
}
else
{
throw e;
}
}
}
if ( hasTheLock )
{
return ourPath;
}
return null;
}
1️⃣-2️⃣-1️⃣ standardLockInternalsDriver.createsTheLock(client, path, localLockNodeBytes);
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
if ( lockNodeBytes != null )//这个判断估计是判断节点是否需要添加多余信息,根据信息有无选择不同的创建节点方法
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);1️⃣-2️⃣-1️⃣-1️⃣ //这个代码是创建临时节点,CreateMode.EPHEMERAL_SEQUENTIAL就是指定节点的类型为临时序列化节点,这里面的forPath(path, lockNodeBytes)是将`父节点路径+子节点名称`["/curator/locks/lock-"]处理成最终的节点路径;第一次进入循环创建节点的代码可能在forPath方法前面,第一次循环发现路径格式不对所以没创建出来节点,在后面方法检查锁是否存在时发现节点没创建因而抛出异常再次进入循环发现路径对了就创建出对应的节点了
}
else
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
return ourPath;
}
1️⃣-2️⃣-2️⃣ lockInternals.internalLockLoop(startMillis, millisToWait, ourPath)
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
boolean haveTheLock = false;//获取到锁的标识默认值为false
boolean doDelete = false;//执行删除的标识位也是false
try
{
if ( revocable.get() != null )
{
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )//检查客户端状态是否已经启动的状态,如果客户端已经启动且没有获取到锁的情况下进入while循环
{
List<String> children = getSortedChildren();//获取用户指定父节点下的所有子节点列表,这个第一次获取子节点列表失败,获取到的是空列表;但是第二次获取成功
String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash,从完整的节点名称中截取出节点不包含父节点路径的名称
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);1️⃣-2️⃣-2️⃣-1️⃣ //判断当前线程对应的序列化节点的序列号是不是子节点列表中最小的,如果是最小的可以获取锁,返回predicateResults对象,里面封装了pathToWatch, getsTheLock属性,如果获取到锁getsTheLock属性就为true,如果没获取到锁就返回前驱节点的名字
if ( predicateResults.getsTheLock() )
{
haveTheLock = true;//如果获取到锁haveTheLock被设置为true会作为该方法的返回值并在上级方法中返回lockPath并在1️⃣方法中封装进threadData的LockData中
}
else//如果获取锁失败就会进入下面给前驱节点设置监听事件的逻辑
{
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this)
{
try
{
// use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
client.getData().usingWatcher(watcher).forPath(previousSequencePath);//在这一步给当前节点下标-最大租约数对应下标的节点添加删除事件监听,这里默认最大租约数为1,所以是给前一个节点添加删除监听事件,官方注释使用getData方法而避免使用exists方法是为了避免不必要的监听导致资源泄露,老师的解释是exists方法不管节点是否存在都会去执行监听,getData节点不存在就不会去监听,那前面自己的实现不是使用的Zookeeper官方客户端的exists方法来检查前驱节点和设置监听事件的吗,那不是也会设置监听事件导致客户端资源泄露,老师承认了之前那个就是exists会存在该问题
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; // timed out - delete our node
break;
}
wait(millisToWait);
}
else
{
wait();
}
}
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
}
}
}
catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
doDelete = true;
throw e;
}
finally
{
if ( doDelete )
{
deleteOurPath(ourPath);//[ourPath: "完整的节点路径"],老师说这里节点验证失败把节点删了,我觉得是他自己都混乱了,真特么难顶啊,这里讲的不清楚;这里只有出现异常才会删除对应路径的节点;成功获取锁doDelete为默认值不会删除对应节点
}
}
return haveTheLock;//第一次执行这里因为抛异常直接返回false,获取锁成功会返回true,返回true作为返回lockPath的判断标志
}
1️⃣-2️⃣-1️⃣-1️⃣ createBuilderImpl.forPath(path, lockNodeBytes)//[path: "/curator/locks/lock-"]
public String forPath(String path) throws Exception
{
return forPath(path, client.getDefaultData());1️⃣-2️⃣-1️⃣-1️⃣-1️⃣ //进入重载的forPath方法
}
1️⃣-2️⃣-2️⃣-1️⃣ standardLockInternalsDriver.getsTheLock(client, children, sequenceNodeName, maxLeases)
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
int ourIndex = children.indexOf(sequenceNodeName);//获取当前节点名字在父节点下的排序后的子节点列表中的下标
validateOurIndex(sequenceNodeName, ourIndex);//验证下标,讲的一坨shit,[sequenceNodeName: _c_09c8be8f-a044-4e1d-a1c7-f50bd4571b8b-lock-序列号],第一次验证下标的过程中抛出异常被1️⃣-2️⃣-2️⃣ 中的catch语句捕获,并将删除标识置为true执行deleteOurPath(ourPath);方法;第二次进来子节点列表存在,验证节点名称和节点下标没抛异常,老师的意思是第一次验证失败把节点删了又重新创建了一次,不知道,我感觉他已经凌乱了,我感觉这里是检验前驱节点是否还存在于Zookeeper服务器的代码
boolean getsTheLock = ourIndex < maxLeases;//如果当前节点的下标小于最大租约,默认值是1,如果是小于就将getsTheLock变量即返回值对象PredicateResults的getsTheLock属性设置为true,表示已经拿到锁而且此时不需要监听前驱节点;如果当前节点下标大于最大租约数说明需要阻塞,此时getsTheLock属性设置为false,且获取前驱节点的名字,这里其实是获取比当前下标小最大租约数的节点名字来进行监听
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
return new PredicateResults(pathToWatch, getsTheLock);
}
1️⃣-2️⃣-1️⃣-1️⃣-1️⃣ createBuilderImpl.forPath(path, client.getDefaultData())
public String forPath(final String givenPath, byte[] data) throws Exception
{
if ( compress )
{
data = client.getCompressionProvider().compress(givenPath, data);
}
final String adjustedPath = adjustPath(client.fixForNamespace(givenPath, createMode.isSequential()));1️⃣-2️⃣-1️⃣-1️⃣-1️⃣-1️⃣ //这里获取到的路径才是节点真正的路径,debug对于[path: "/curator/locks/lock-"]的返回结果是[adjustedPath: "/curator/locks/_c_09c8be8f-a044-4e1d-a1c7-f50bd4571b8b-lock-"],即在lock-前面加了一个`_c_uuid-`,因此节点最终的路径为"用户指定父节点/_c_uuid-lock-ZK生成的序列号",其实这个方法就做了一个字符串拼接,没什么技术含量,主要还是根据protectedMode.doProtected属性如果为true才去拼接最终的节点路径
List<ACL> aclList = acling.getAclList(adjustedPath);//获取acl权限列表
client.getSchemaSet().getSchema(givenPath).validateCreate(createMode, givenPath, data, aclList);//验证路径是否合法
String returnPath = null;
if ( backgrounding.inBackground() )
{
pathInBackground(adjustedPath, data, givenPath);
}
else
{
String path = protectedPathInForeground(adjustedPath, data, aclList);
returnPath = client.unfixForNamespace(path);
}
return returnPath;//返回最终的路径[returnPath: "/curator/locks/_c_09c8be8f-a044-4e1d-a1c7-f50bd4571b8b-lock-"]
}
1️⃣-2️⃣-1️⃣-1️⃣-1️⃣-1️⃣ createBuilderImpl.adjustPath(client.fixForNamespace(givenPath, createMode.isSequential()))
String adjustPath(String path) throws Exception//这里传进来的path值为[path: "/curator/locks/lock-"]
{
if ( protectedMode.doProtected() )//doProtected()方法是返回protectedMode对象中的布尔类型属性doProtected,该属性属性值默认为false,当createBuilderImpl对象实例化时会根据构造方法传参的布尔类型参数doProtected决定是否将protectedMode.doProtected属性调用protectedMode.setProtectedMode()将其改为true并生成UUID存入protectedMode.protectedId中,protectedMode对象在createBuilderImpl的protectedMode属性声明时就创建出来了
{
ZKPaths.PathAndNode pathAndNode = ZKPaths.getPathAndNode(path);//传参[path: "/curator/locks/lock-"]返回ZKPaths.PathAndNode对象,该对象中主要有两个属性,其中path属性是用户自定义的父节点路径,node属性是固定的`lock-`字符串
String name = getProtectedPrefix(protectedMode.protectedId()) + pathAndNode.getNode();//在ZKPaths.PathAndNode对象的node属性前拼上一堆前缀,这里看到了protectedMode.protectedId()就是前面提到的UUID,该UUID在createBuilderImpl初始化时生成,而且getProtectedPrefix()方法还在UUID前面加了_c_,`_c_`是以字符串常量PROTEGTED_PREFIX存在于CreateBuilderImpl中的,getProtectedPrefix()方法拼接的是PROTEGTED_PREFIX+protectedMode.protectedId()+"-"
path = ZKPaths.makePath(pathAndNode.getPath(), name);//再把ZKPaths.PathAndNode对象的path属性即用户自定义的父节点路径直接拼接到PROTEGTED_PREFIX+protectedMode.protectedId()+"-"+"lock-"前面再加一个"/"
}
return path;//返回Sting类型的该路径一路到分支1️⃣-2️⃣-1️⃣ 处
}
字体样式
黑体:SimHei |
华文黑体:STHeiti |
微软正黑体:Microsoft JhengHei |
微软雅黑体:Microsoft YaHei |
华文细黑:STXihei |
楷体:KaiTi |
楷体_GB2312:KaiTi_GB2312 |
华文楷体:STKaiti |
华文行楷:STXingkai |
|
宋体:SimSun |
新宋体:NSimSun |
仿宋:FangSong |
仿宋_GB2312:FangSong_GB2312 |
华文宋体:STSong |
华文中宋:STZhongsong |
华文仿宋:STFangsong |
等宽代码:Monospace |
||
华文新魏:STXinwei |
华文琥珀:STHupo |
隶书:LiSu |
华文隶书:STLiti |
华文彩云:STCaiyun |
方正舒体:FZShuTi |
方正姚体:FZYaoti |
幼圆:YouYuan |
PingFangSC-Medium |
标题简介是对标题的一种简单解释或者背景介绍,用两条横线括起来,中间就是现在的内容,以这种方式来表示
如果是一级或者二级标题对应标题已经自带一条分割线,此时不再额外添加分割线,直接使用无序列表对标题进行简介说明
要点
Typora无法执行JS代码,但是网页可以渲染JS代码,把摘要栏的最后更新时间设置成如果JS代码能执行,则用当前文件的最后修改时间覆盖时间图层的文本内容,JS渲染的时间格式示例为Wed Apr 16 2024
Typora导出PDF时对前端代码的渲染效果并不好,常常会发生前端代码效果混乱的现象【也可能是html格式涉及到页面组合的代码需要独占行,否则出现页面渲染布局异常的问题】
注意一下摘要栏JS代码块中的window.onload
只会执行其中一个,正常是执行最后写的一个,如果一个文档中存在多个window.onload
事件中执行的代码,需要将这些模板中的事件代码整合在同一个window.onload
中,否则部分时间无法转换为JS代码渲染的时间,如下示例所示【这些组件代码不需要完整的html结构,JS代码块可以写在body标签中】,直接用浏览器就可以显示以下代码效果,注意这种html格式的样式必须独占行,否则会发生Typora中正常渲染但是转换成html或者PDF渲染效果混乱的情况
基础版
🔎:注意描述文本太长时,需要设置图层div的层高大于文本内容的总高度,才能实现描述内容的整体对齐效果,一旦文字高度大于图层高度,多出来的文字会换行从行首开始展示,单行文字高度约20px
,图层间隔约20px
,总的图片背景图层高为20x(标签数+1)+20x文字行数px
,这种方式还存在当浏览器或者窗口宽度变化导致文字总高度变小会出现行间距变大的现象,还可以继续优化
Git版
Typora可以正常渲染前端代码,这意味着Markdown语法和前端语法可以混写
更牛逼的是MD文档可以被渲染成效果相同的前端页面和PDF,这意味着在Typora中可以随心所欲地写网页,同时还可以在线下日常编辑网站内容,只要简单转换部署就能极速更新网站内容
Typora中只要是标题内容可以按标题层级在网页生成目录,并且可以检索跳转对应的标题位置
快捷键 | 作用说明 |
---|---|
Ctrl+U | 文字添加下划线 |
Alt+Shift+5 | 文字添加删除线 |
Ctrl+K | 插入链接 |
Ctrl+Shift+1 | 隐藏侧边栏或者打开大纲视图 |
Ctrl+Shift+K | 插入代码块 |
Ctrl+Shift+M | 插入公式块 |
Crtl+Home | 跳转文档开头 |
Ctrl+F/F3 | 搜索当前文档内容 |
Crtl+End | 跳转文档末尾 |
Ctrl+Shift+I | 插入图片 |
F11 | 切换全屏 |
Ctrl+/ | 打开源代码编辑模式 |
Ctrl+H | 查找替换 |
Tab | 向后缩进 |
Shift+Tab | 向前缩进 |
Ctrl+T | 插入表格 |
Ctrl+Shift+Q | 生成说明栏 |
Ctrl+B | 粗体文字 |
Ctrl+I | 斜体文字 |
Ctrl+# | #表示大键盘数字,将文本转换成#级标题 |
Ctrl+Enter | 光标在表格中使用此快捷键快速在光标下方插入行 |
Ctrl+Shift+1 | 隐藏显示左侧边栏 |
Ctrl+Shift+x | 生成复选框 |
Shift+Enter | 生成换行标签<br /> 这样就可以在表格中实现换行效果 |
Ctrl+Shift+Delete | 删除表格中光标所在的当前行 |
在段落头部输入+,然后点击空格
Typora中的无序列表采用的html标签中的无序列表<ul>
来直接渲染的,样式少,而且嵌入自定义的html在导出HTML或者PDF时涉及页面布局的效果往往会被错乱渲染,经常Typora中能用的HTML代码片段,转成HTML或者PDF就不能用了
一级无序列表
二级无序列表
三级以及多级无序列表
在段落头部输入1.,然后点击空格
样式示例
在段落头部输入+ [ ]
,会生成可选中选框,必须严格输入+空格[空格]空格
,否则会变成无序列表
可以缩进使用
输入**
,星号间的文字将会展现为斜体
输入****
,四个星号最中间的文字将会展现为粗体
输入******
,六个星号最中间的文字将会展现为斜体加粗
输入~~
,波浪线中间的文字将会展现为下标
输入~~~~
,四个波浪线最中间的文字将会展现为删除线
输入^^
,尖号之间的文字将会展现为上标
输入====
,四个等号之间的文件将会高亮展现key
文字头部输入#
,并点击空格,井号+空格
后面字体将会整体展现为标题【独占行为标题】或者大好字体一号字体【非独占行为大好字体】
🔎:几级标题或者小号字体就输入几个井号,最多可以输入6个井号
输入~~~
,生成代码块
输入$$
,点击回车,生成公式块,这个公式块不一定能被网页渲染出来,但是可以被转成PDF
输入>
,点击空格生成说明栏
输入\
,转义字符可以对emoji和特殊字符进行转义,直接输出文本,不再转换为对应效果,如:mag_right:
输入<u></u>
,夹在<u>标签中的内容将会展示为下划线
参考:https://blog.csdn.net/dream_summer/article/details/110822636
Latex语法也可以制表,效果似乎比MarkDown和HTML的效果更好,但是MarkDown中的Latex似乎不支持制表的Latex语法,深入了解一下
Excel导出的时候可以直接导出为Html,研究一下Excel导出的源码
鼠标右键--插入--表格即可插入表格
🔎:表格中的数据可以是图片
合并行列
列一 | 列二 |
合并行 | |
列一 | 列二 |
合并列 | 行二列二 |
行三列二 |
[合并列示例]
大类 | 数据类型 | 占用存储空间[Byte] | 数字范围 | 默认值 |
整数型 | ||||
byte | 1 | -128~127[27-1] | 0 | |
short | 2 | 约-32000~32000[215-1] | ||
int | 4 | 约-21亿~21亿[215-1] | ||
long | 8 | [263-1] | 0L | |
布尔型 | boolean | 1 | true |false |
false |
字符型 | char | 2 | 约0~6500[216-1] | \u0000 |
浮点型 | float | 4 | 有效位数6-7位[3*1038] | 0.0 |
double | 8 | 有效位数15位[1.8*10308] |
单行代码
输入``,在中间写入单行代码
样式示例:import com.atlisheng.array.ArrayList;
代码块
输入```,在后面紧跟代码语言种类并回车
样式示例
xxxxxxxxxx
public static void main(String[] args){
show();
}
输入三个连续减号就会生成分割线
🔎可以单独成行,也可以搭配其他样式使用
typora
的图片插入为鼠标右键--插入--图像,在输入图片路径的提示括号中输入图片的本地绝对路径或者使用相对路径将md文件和图片保存在同一个文件目录内【这种方式文件传递过程中图片需要打包一起进行传递,否则图片在别的机器上无法加载】,或者直接使用图床服务器、对象存储服务器、自己服务器上图片对应的访问URL
就可以直接访问【这种方式转发文件无需携带图片包,但是需要对图片进行网络存储】
当然也可以像摘要栏一样直接写前端代码,在前端代码中按前端的方式插入图片
插入网络存储图片效果演示
图片靠左
同样通过前端代码style="zoom:8%;float:left"
属性来控制图片靠左或者靠右【靠右改成right即可】
但是这种方式导出PDF会有问题,更好的方式是修改主题样式.css文件,参考Typora图片居左、中、右(推荐方式二)-CSDN博客【但是我这里转成PDF是可行的】
输入[toc]
,自动生成文档目录,该文档目录在PDF或者html页面都是可以正常显示和使用的
MD文档个人规范流程图嵌入网页时序图Emoji图标HTML4实体规范样式参数解析指令解析API解析WEB API解析内容确认步骤流程常见问题测试分析补充说明旁注存疑和解惑源码解析字体文档布局标题简介摘要栏常用模板Typora快捷键文档样式无序列表有序列表复选框脚注文字样式表格代码分割线图片文档目录Latex公式流程图嵌入网页时序图文档找回吃灰库扩展无序列表附录
输入$$后回车获取公式框,输入Latex语法符号被渲染为特定的公式
Latex语法
语法 | 对应公式 |
---|---|
$%$ | 注释,百分号后面的内容作为注释内容 |
$\%$ | 百分号是注释标识符,使用转义字符\保留百分号的原始意义 |
流程图简介
这个流程图可以被原样输出到PDF中,也可以被原样渲染成html页面
Typora之所以可以画流程图是因为支持mermaid语言,所以使用只需要插入代码块,把语言调成mermaid即可
官方的介绍网址:https://mermaid-js.github.io/mermaid/#/flowchart?id=a-hexagon-node
Mermaid是一个用于画流程图、状态图、时序图、甘特图的库,使用 JS 进行本地渲染,广泛集成于许多Markdown编辑器中
流程图代码示例
网页嵌入示例
Typora有时候文件会全白,盗版遇到这种情况比较多,这种情况文本内容还没丢,渲染出了问题,一般改改文本就能恢复正常
出现电脑断电Typora正在编辑的文件也可能丢,文本内容全变成不可读字符,更改编码格式也不管用,这时候文件就是真丢了,在下有幸断电遇到过,此时可以通过Typora内部保存的历史草稿找回
这些历史记录在默认目录C:\Users\计算机用户名\AppData\Roaming\Typora\draftsRecover
中,一般两三分钟就会保存一份草稿,起码保存好几个小时的记录,可以在其中找到最近的草稿另存一份就能恢复
此外这个历史草稿目录还可以通过菜单文件--偏好设置--点击恢复未保存的草稿直接跳转到对应的历史草稿目录
🔎
🔎
$remote_addr
是远程IP地址、$time_local
是本地时间、$status
是请求的状态,还有以前使用过的各种变量,貌似官网列举的变量不是全部,老师说正常的Nginx变量都可以用
除了搭建博客的vuePress
框架,再了解一下vitePress
,据说比vuePress
更好,查了一下vitePress
不适合做博客,国内专门做博客的框架如hexo
,hugo
这些都挺不错的,vitePress
适合做一些开源项目的官网
他这里的博客意思可能是零散的文章,我要做的就是像企业官方文档一样的知识管理平台,说不定vitePress
正合适
Typora相关的使用说明可以参考Typora 中的 HTML 支持 | TyporaChina